import game from "./game.js";


const N_UCT_TIMES_MIN = 16384
const N_UCT_TIMES_MAX = 1048576
const N_UCT_TIMES_DEFAULT = N_UCT_TIMES_MIN
const N_LEVEL_MIN = 0
const N_LEVEL_MAX = 9
const N_LEVEL_DEFAULT = 1
const N_PIECE_FULLTREE = game.BOARD_LEN - 13
const SAY_HAS_WIN = "(●'◡'●)"
const SAY_MAYBE_WIN = "(★ ω ★)"
const SAY_HAS_LOSE = "(T_T)"
const SAY_UNKNOWN = "ಠ_ಠ"
const MS_STEP_REPORT = 200

const UCB_C = 1.6487212707001281
const UCB_C_END = 1.3
const UCB_C_DW = UCB_C - UCB_C_END

const VALAUATION_INIT = [
    9.9, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 9.9,
    0.7, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.7,
    0.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6,
    0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5,
    0.4, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.4,
    0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3,
    0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2,
    0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1,
    0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2,
    0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3,
    0.4, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.4,
    0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5,
    0.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6,
    0.7, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.7,
    9.9, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 9.9,
]

const Out = {
    reply(...args){
        self.postMessage(args.join(' '))
    },
    reply_ok(...args){
        Out.reply("ok", ...args)
    },
    report_error(msg){
        self.postMessage(`[ERROR] ${msg}`)
    },
}

const State = {
    _uct_times: N_UCT_TIMES_DEFAULT,
    _level: N_LEVEL_DEFAULT,
    uct_times(to=undefined){
        if (R.is(Number,to)) {
            State._uct_times = Math.max(N_UCT_TIMES_MIN, Math.min(N_UCT_TIMES_MAX, to))
        }
        return State._uct_times
    },
    level(to=undefined){
        if (R.is(Number,to)) {
            State._level = Math.max(N_LEVEL_MIN, Math.min(N_LEVEL_MAX, to))
        }
        return State._level
    },
}

function rand_range(a,b){
    return Math.floor(Math.random() * (b-a)) + a
}

function swap_remove(xs,i) {
    const tail_i = xs.length - 1
    const del = xs[i]
    xs[i] = xs[tail_i]
    xs.splice(tail_i)
    return del
}

function valuation_n_pieces(n_black, n_white) {
    if ((n_black + n_white) >= game.BOARD_LEN) {
        if (n_black == n_white) {
            return 0.0
        } else if (n_black < n_white) {
            return 1.0
        } else {
            return -1.0
        }
    } else {
        const n_diff = n_white - n_black
        const diff_way = (2.0 * (n_diff >= 0.0)) - 1.0
        return (1.0 - (1.0 / (1.0 + Math.abs(n_diff)))) * diff_way
    }
}

function valuation_init(board) {
    let whole = 0.0
    for (const loc of R.range(0, game.BOARD_LEN)) {
        whole += VALAUATION_INIT[loc] * game.piece_way(board.see(loc))
    }
    return whole
}

function step_full_tree(pcur, board, depth_max) {
    let count = 0
    function search(pcur, board, depth, alpha, beta) {
        if (depth > 0) {
            let moves = board.moves(pcur)
            if (moves.length > 0) {
                const node_board = new game.Board(board)
                while (moves.length > 0) {
                    const loc = swap_remove(moves, rand_range(0, moves.length))
                    node_board.down(loc, pcur, true)
                    const value = -search(game.piece_side(pcur), node_board, depth-1, -beta, -alpha)
                    alpha = Math.max(alpha, value)
                    if (alpha >= beta) {break}
                    node_board.reset(board)
                }
                return alpha
            } else {
                return -search(game.piece_side(pcur), board, depth-1, -beta, -alpha)
            }
        } else {
            count += 1
            return valuation_n_pieces(board.n_black(), board.n_white()) * game.piece_way(pcur)
        }
    }
    const moves = board.moves()
    let hit_value = 1.0
    let hit_loc = moves[0]
    while (moves.length > 0) {
        const loc = swap_remove(moves, rand_range(0, moves.length))
        const node_board = new game.Board(board)
        node_board.down(loc, pcur, true)
        let value = search(game.piece_side(pcur), node_board, depth_max, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)
        Out.reply("status", `Node ${count}`)
        if (value < hit_value) {
            hit_value = value
            hit_loc = loc
            if (value <= -1.0) {break}
        }
    }
    let say = SAY_UNKNOWN
    if (hit_value <= -1.0) {say = SAY_HAS_WIN}
    else if (hit_value <= -0.5) {say = SAY_MAYBE_WIN}
    else if (hit_value > 0.0) {say = SAY_HAS_LOSE}
    Out.reply("say", say)
    return hit_loc
}

function step_uct(pcur, board, uct_times, level, ucb_c, ms_report) {
    class Node {
        constructor(pcur, loc, board, _parent=undefined) {
            this.parent = _parent
            this.pcur = pcur
            this.loc = loc
            this.n_select = 0
            this.n_wins = 0.0
            this.nexts = []
            this._next_moves = board.moves()
        }
        has_fully() {return this._next_moves.length < 1}
        expend(board) {
            const side = game.piece_side(this.pcur)
            let loc = game.LOC_INVALID
            if (this._next_moves.length > 0) {
                let hit_i = rand_range(0, this._next_moves.length)
                loc = swap_remove(this._next_moves, hit_i)
                board.down(loc, side, true)
            }
            let next = new Node(side, loc, board, this)
            this.nexts.push(next)
            return next
        }
        avg_wins() {return this.n_wins / this.n_select}
    }
    function make_root(pcur, board) {
        return new Node(game.piece_side(pcur), game.LOC_INVALID, board)
    }
    function ucb(root, ucb_c) {
        const root_N = 2.0 * Math.log(1.0 + root.n_select)
        let max_bonus = Number.MIN_SAFE_INTEGER
        let max_node = root.nexts[0]
        for (const node of root.nexts) {
            if (node.n_select < 1) {
                return node
            }
            const c = (node.avg_wins() < 0.0) ? UCB_C : ucb_c
            const explore = ucb_c * Math.sqrt(root_N / node.n_select)
            const rewind = node.avg_wins()
            const bonus = explore + rewind
            if (max_bonus < bonus) {
                max_bonus = bonus
                max_node = node
            }
        }
        return max_node
    }
    function select(root, time_board, ucb_c) {
        if (root.has_fully()) {
            const next = ucb(root, ucb_c)
            if (game.loc_in_board(next.loc)) {
                time_board.down(next.loc, next.pcur, true)
            }
            return select(next, time_board, ucb_c)
        } else {
            return root.expend(time_board)
        }
    }
    const _sim_board = new game.Board()
    function simulate(node, time_board, sr) {
        _sim_board.reset(time_board)
        const moves = _sim_board.moves()
        let n_piece = _sim_board.n_piece()
        let side = game.piece_side(node.pcur)
        while (n_piece < game.BOARD_LEN) {{
            const hit_i = rand_range(0, moves.length)
            const loc = swap_remove(moves, hit_i)
            _sim_board.down(loc, side, true)
            side = game.piece_side(side)
            n_piece += 1
        }}
        sr[0] = valuation_n_pieces(_sim_board.n_black(), _sim_board.n_white()) * game.piece_way(node.pcur)
    }
    function backup(node, sr) {
        function layer(leaf, node, sr) {
            node.n_select += 1
            const add_way = (2.0 * (leaf.pcur == node.pcur)) - 1.0
            node.n_wins += sr[0] * add_way
            if (R.is(Node, node.parent)) {
                layer(leaf, node.parent, sr)
            }
        }
        layer(node, node, sr)
    }
    function report(time_last, times) {
        const time_now = Date.now()
        if ((time_now - time_last) >= ms_report) {
            Out.reply("status", `UCT ${uct_times - times}`)
            return time_now
        } else {
            return time_last
        }
    }
    Out.reply_ok("step-uct", `N=${uct_times} L=${level}`)
    const root = make_root(pcur, board)
    let time_last = Date.now()
    const time_board = new game.Board(board)
    const sr = new Float32Array(1)
    for (const times of R.range(0, uct_times)) {
        const node = select(root, time_board, ucb_c)
        simulate(node, time_board, sr)
        backup(node, sr)
        time_board.reset(board)
        time_last = report(time_last, times)
    }
    root.nexts.sort((a,b)=>(b.n_select - a.n_select))
    root.nexts.splice(8)
    let dstr = ""
    for (const node of root.nexts) {
        const row = `[${node.loc}] N=${node.n_select} Q=${node.avg_wins()}`
        dstr = `${dstr}${row}\n`
    }
    console.log(dstr)
    const hit_node = root.nexts[0]
    Out.reply("status", `Loc ${hit_node.loc}`)
    const p_winrate_str = String(Math.trunc(hit_node.avg_wins() * 100.0))
    Out.reply("say", `${p_winrate_str}%`)
    return hit_node.loc
}

const INIT_LEVEL_DEPTHS = [3,3,4,4,4,5,5,6,7,8]
function step_init(pcur, board, level, ms_report) {
    function report(time_last, n) {
        const time_now = Date.now()
        if ((time_now - time_last) >= ms_report) {
            Out.reply("status", `INIT ${n}`)
            return time_now
        } else {
            return time_last
        }
    }
    function init_moves(pcur, board) {
        const moves = board.moves(pcur)
        return moves.filter((loc)=>(VALAUATION_INIT[loc] > 0.0))
    }
    let time_last = Date.now()
    let count = 0
    function search(pcur, board, depth, alpha, beta) {
        if (depth > 0) {
            const moves = init_moves(pcur, board)
            if (moves.length > 0) {
                const node_board = new game.Board(board)
                while (moves.length > 0) {
                    const loc = swap_remove(moves, rand_range(0, moves.length))
                    node_board.down(loc, pcur, true)
                    const value = -search(game.piece_side(pcur), node_board, depth-1, -beta, -alpha)
                    alpha = Math.max(alpha, value)
                    if (alpha >= beta) {break}
                    node_board.reset(board)
                }
                return alpha
            } else {
                return -search(game.piece_side(pcur), board, depth-1, -beta, -alpha)
            }
        } else {
            count += 1
            time_last = report(time_last, count)
            return valuation_init(board) * game.piece_way(pcur)
        }
    }
    const corners = board.moves().filter((loc)=>(VALAUATION_INIT[loc] > 1.0))
    if (corners.length > 0) {
        return corners[rand_range(0, corners.length)]
    }
    const depth_max = INIT_LEVEL_DEPTHS[level]
    const nodes = []
    const time_board = new game.Board(board)
    const side = game.piece_side(pcur)
    for (const loc of board.moves()) {
        time_board.down(loc, pcur, true)
        const whole = -search(side, time_board, depth_max, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)
        nodes.push([loc, whole])
        time_board.reset(board)
    }
    nodes.sort((a,b)=>(b[1] - a[1]))
    nodes.splice(8)
    let dstr = ""
    for (const node of nodes) {
        const row = `[${node[0]}] W=${node[1]}`
        dstr = `${dstr}${row}\n`
    }
    console.log(dstr)
    const hit_node = nodes[0]
    Out.reply("status", `Loc ${hit_node[0]}`)
    return hit_node[0]
}

const INIT_NUMS = VALAUATION_INIT.filter((a)=>(a > 0.0)).length
function step(pcur, board) {
    const n_piece = board.n_piece()
    let phase = n_piece / game.BOARD_LEN
    const n_boared = board.foreach((loc,p,n)=>{
        if (VALAUATION_INIT[loc] * game.piece_way(p) != 0.0) {return 1+n}
        return n
    }, 0)
    const rate = n_boared / INIT_NUMS
    if (rate < 0.9) {
        Out.reply("status", "INIT")
        return step_init(pcur, board, State.level(), MS_STEP_REPORT)
    } else if (n_piece < N_PIECE_FULLTREE) {
        Out.reply("status", "UCT")
        const ucb_c = UCB_C - (UCB_C_DW * phase)
        return step_uct(pcur, board, State.uct_times(), State.level(), ucb_c, MS_STEP_REPORT)
    } else {
        Out.reply("status", "FULL TREE")
        return step_full_tree(pcur, board, game.BOARD_LEN - N_PIECE_FULLTREE)
    }
}

const Take = {}
Take["fuck"] = (board_str)=>{
    const board = new game.Board(board_str)
    Out.reply_ok("fuck", valuation_init(board))
}
Take["step"] = (pcur_str, board_str)=>{
    const pcur = game.piece_from_str(pcur_str)
    const board = new game.Board(board_str)
    Out.reply("step", step(pcur, board))
}
Take["set"] = (name, val)=>{
    const fns = {}
    fns["uct-times"] = (str)=>{return State.uct_times(Number(str))}
    fns["level"] = (str)=>{return State.level(Number(str))}
    if (fns.hasOwnProperty(name)) {
        Out.reply_ok("set", name, fns[name](val))
    } else {
        Out.report_error(`set param '${name}' no exist`)
    }
}
Take["reset"] = ()=>{
    console.log("(reset)")
}
Take["raii"] = ()=>{
    console.log("(raii)")
}

;(()=>{
    self.onmessage = function(e){
        const tokens = e.data.split(' ')
        if (tokens.length > 0){
            const head = tokens[0]
            if (Take.hasOwnProperty(head)) {
                Take[head](...(tokens.slice(1)))
            } else {
                Out.report_error(e.data)
            }
        }
    }
    Out.reply("ready")
})()
